/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2012, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.feature;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.geotools.api.feature.Feature;
import org.geotools.api.feature.FeatureFactory;
import org.geotools.api.feature.Property;
import org.geotools.api.feature.type.AttributeDescriptor;
import org.geotools.api.feature.type.FeatureType;
import org.geotools.api.feature.type.Name;
import org.geotools.api.feature.type.PropertyDescriptor;
import org.geotools.factory.CommonFactoryFinder;

/**
 * The complex feature builder allows the construction of features by progressively appending their
 * components and deferring the construction till you're ready.
 *
 * @author Adam Brown (Curtin University of Technology)
 */
public class ComplexFeatureBuilder extends FeatureBuilder<FeatureType, Feature> {
    Map<Name, ArrayList<Property>> values = new HashMap<>();

    AttributeDescriptor ad = null;

    public ComplexFeatureBuilder(FeatureType featureType) {
        this(featureType, CommonFactoryFinder.getFeatureFactory(null));
    }

    protected ComplexFeatureBuilder(FeatureType featureType, FeatureFactory factory) {
        super(featureType, factory);
    }

    public ComplexFeatureBuilder(AttributeDescriptor ad) {
        this(ad, CommonFactoryFinder.getFeatureFactory(null));
    }

    protected ComplexFeatureBuilder(AttributeDescriptor ad, FeatureFactory factory) {
        super((FeatureType) ad.getType(), factory);
        this.ad = ad;
    }

    /**
     * Build and return the feature you've been constructing. If the id is null it will be assigned
     * from FeatureBuilder.createDefaultFeatureId().
     */
    @Override
    public Feature buildFeature(String id) {
        // Instantiate if null:
        id = id == null ? FeatureBuilder.createDefaultFeatureId() : id;

        // Validate the values against the featureType; we need to make sure
        // that requirements are honoured:
        for (PropertyDescriptor propertyDescriptor : super.featureType.getDescriptors()) {
            Name name = propertyDescriptor.getName();

            // Create a List of Properties for this name if we don't already
            // have one:
            if (!values.containsKey(name)) {
                values.put(name, new ArrayList<>());
            }

            // Get the List of Properties:
            List<Property> properties = values.get(name);

            // See if there's a mismatch between the number of properties and
            // minOccurs value:
            int minOccurs = propertyDescriptor.getMinOccurs();
            int numberOfProperties = properties.size();

            if (numberOfProperties < minOccurs) {
                // If the value is nillable anyway then just default it to null:
                if (propertyDescriptor.isNillable()
                        && AttributeDescriptor.class.isAssignableFrom(
                                propertyDescriptor.getClass())) {
                    do {
                        Property nullProperty =
                                new AttributeImpl(
                                        propertyDescriptor.getType().getBinding().cast(null),
                                        (AttributeDescriptor) propertyDescriptor,
                                        null);

                        properties.add(nullProperty);
                    } while (++numberOfProperties < minOccurs);
                }
                // NOTE: I was wondering if you could have another if-else here
                // to try to apply default values if they're set..
                // it seems like a good idea but the only problem is that
                // they're only present on the AttributeDescriptors...
                else {
                    throw new IllegalStateException(
                            String.format(
                                    "Failed to build feature '%s'; its property '%s' requires at least %s occurrence(s) but number of occurrences was %s.",
                                    featureType.getName(), name, minOccurs, numberOfProperties));
                }
            }
        }

        // Merge the Map<String, ArrayList<Property>> into one collection of
        // properties:
        Collection<Property> properties = new ArrayList<>();
        for (Name key : values.keySet()) {
            properties.addAll(values.get(key));
        }

        this.values.clear();
        if (ad != null) {
            return factory.createFeature(properties, ad, id);
        } else {
            return factory.createFeature(properties, featureType, id);
        }
    }

    /**
     * Append a property value to the complex feature under construction and associate it with the
     * name specified.
     *
     * @param name The name of the property you wish to set.
     * @param value The value of the property to append.
     */
    public void append(Name name, Property value) {
        PropertyDescriptor propertyDescriptor = featureType.getDescriptor(name);

        // The 'name' must exist in the type, if not, throw an exception:
        if (propertyDescriptor == null) {
            throw new IllegalArgumentException(
                    String.format(
                            "The name '%s' is not a valid descriptor name for the type '%s'.",
                            name, this.featureType.getName()));
        }

        Class<?> expectedClass = propertyDescriptor.getType().getBinding();
        if (value != null) {
            Class<?> providedClass = value.getType().getBinding();

            // Make sure that the provided class and the expected class match or
            // that the expectedClass is a base class of the providedClass:
            if (!providedClass.equals(expectedClass)
                    && !expectedClass.isAssignableFrom(providedClass)) {
                throw new IllegalArgumentException(
                        String.format(
                                "The value provided contains an object of '%s' but the method expects an object of '%s'.",
                                providedClass, expectedClass));
            }
        } else { // value == null
            if (propertyDescriptor.isNillable()) {
                value = (Property) expectedClass.cast(null);
            } else {
                // NOTE: This could possibly to changed to allow for processing
                // remote xlinks.
                value = (Property) expectedClass.cast(null);
            }
        }

        // At this point the converted value has been set so we must persist it
        // to the object's state:
        ArrayList<Property> valueList;

        if (values.containsKey(name)) {
            valueList = values.get(name);

            // Make sure that the list isn't already at capacity:
            int maxOccurs = propertyDescriptor.getMaxOccurs();
            if (valueList.size() == maxOccurs) {
                throw new IndexOutOfBoundsException(
                        String.format(
                                "You can't add another object with the name of '%s' because you already have the maximum number (%s) allowed by the property descriptor.",
                                name, maxOccurs));
            }
        } else {
            valueList = new ArrayList<>();
            values.put(name, valueList);
        }

        valueList.add(value);
    }
}
